-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RDP's in TS OSDK #1035
base: main
Are you sure you want to change the base?
RDP's in TS OSDK #1035
Conversation
) => this; | ||
} | ||
|
||
type CollectAggregations = "collectSet" | "collectList"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parroting gateway names for now. This probably needs a review into valueSet
or valueList
, but the gateway names should be fine for right now
}); | ||
}); | ||
|
||
it("propagates derived property type to future object set operations with correct types", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test and others with no clause are just for us to ensure that we're typing things correctly.
} from "../ontology/ObjectTypeDefinition.js"; | ||
import type { LinkedType, LinkNames } from "../util/LinkUtils.js"; | ||
import type { WithPropertyDefinition } from "./WithPropertiesClause.js"; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use inheritance here to make sure we enforce what methods are available and to reduce duplication
SingleLinkWithPropertyObjectSet<Q> | ||
{} | ||
|
||
export interface BaseWithPropertyObjectSet< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This object set only has filter and pivot as aggregations and selectProperty is not available on the base object set.
>; | ||
} | ||
|
||
interface SingleLinkWithPropertyObjectSet< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BaseObjectSet
extends SingleLinkWithPropertyObjectSet
. This makes the selectProperty
method available only for chains of single links, which correlates with the backend. The pivotTo
operation in BaseObjectSet
enforces that if we pivot to a many link, we from then on use ManyLinkWithPropertyObjectSet
which does not have selectProperty
available and only returns further ManyLink object sets.
D extends WithPropertiesClause<K>, | ||
> = { | ||
__DefinitionMetadata: { | ||
properties: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all we need to propagate the types through all further object set operations. The FLUP here is to make the type of the whole object set extractable and definable so that a user can pass around object sets that are correctly typed with the additional properties and do not have to be inferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looking good! Left some minor comments
interface ManyLinkWithPropertyObjectSet< | ||
Q extends ObjectOrInterfaceDefinition, | ||
> extends AggregatableWithPropertyObjectSet<Q> { | ||
readonly pivotTo: <L extends LinkNames<Q>>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So just clarifying, the reason we need to duplicate pivotTo
here is because if there's a many link anywhere in the clause, you can't select anymore? Even if you go from a many to a single link?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's correct, which makes sense because for this to work you have to basically be navigating to exactly one object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably document that here too.
|
||
interface AggregatableWithPropertyObjectSet< | ||
Q extends ObjectOrInterfaceDefinition, | ||
> extends FilterableDeriveObjectSet<Q> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this have to extend FilterableDeriveObjectSet<Q>
purely to make ManyLinkWithPropertyObjectSet
work? If so, could we just have that extend FilterableDeriveObjectSet<Q>
as well? Feels a bit confusing the way its setup now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge deal though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! Agreed this makes it more readable
) => this; | ||
} | ||
|
||
type CollectAggregations = "collectSet" | "collectList"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: for this and the type below can we call them something like CollectDeriveAggregations
, really want to make sure we don't confuse these with existing normal agg options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debating between the above Derive, ow WithPropertyBaseAggregations, or BaseWithPropertyAggregations. We aren't using "derive" as a term, but it is much cleaner than the other 2. Number 3 is kind of confusing, but number 2 would mean they al start the same which I don't want. Stuck with number 1 for conciseness
export type WithPropertyDefinition< | ||
T extends ObjectMetadata.Property, | ||
> = { | ||
definitionId: unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not narrow this type down at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can type this down to a string, I was wondering if we should type this as unknown to tell the user -> dont touch this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typed it as a string even though we could use a number in case we decide we want to use a UUD or something else in the future
@@ -36,14 +36,19 @@ type AGG_FOR_TYPE<T> = number extends T ? NumericAggregateOption | |||
|
|||
export type ValidAggregationKeys< | |||
Q extends ObjectOrInterfaceDefinition, | |||
StringOptions extends string = StringAggregateOption, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to keep all the agg options a bit simpler to parse though, another way to format this type could be keeping the Agg_For_Type
and modifying it to take an extra boolean (that controls whether this is for derive or not, default to false) and have it branch to the right agg options that way, rather than having the responsibility on the user to pass in custom options if they want. That being said, if you feel like in the future we'll need to customize agg options even more, this does make it more flexible so up to you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do keep it like this, lets remove AGG_FOR_TYPE
if its not being used anywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah this does make sense to me, will refactor
type: "searchAround", | ||
objectSet, | ||
link, | ||
}, definitionMap) as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is as any
the best we can do here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we don't need it, this was left over from dev
// Executed code fails since we're providing bad strings to the function | ||
it.fails("correctly narrows types of aggregate function", () => { | ||
client(Employee).withProperties({ | ||
"derivedPropertyName": (base) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
me being annoying nit: but for completeness, lets add an empty string arg as well and make sure that errors
it("allows fetching derived properties with correctly typed Osdk.Instance types", async () => { | ||
const objectWithRdp = await client(Employee).withProperties({ | ||
"derivedPropertyName": (base) => | ||
base.pivotTo("lead").selectProperty("employeeId"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a test to verify things don't break if we select a property that has all undefined values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically if a prop returns an undefined value
describe("Derived Properties Object Set", () => { | ||
it("does not allow aggregate or selectProperty before a link type is selected", () => { | ||
client(Employee).withProperties({ | ||
"derivedPropertyName": (base) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add a test to make sure that within a WithPropertiesClause you can't just return something like base.pivotTo("lead")
without doing some sort of selection or aggregation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies in advance if I missed this somewhere, but don't seem to see that being tested anywhere yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is guarded by the type of the lambda, but I will add an explicit test for this behavior
: never; | ||
|
||
export type ValidAggregationKeys< | ||
Q extends ObjectOrInterfaceDefinition, | ||
StringOptions extends string = StringAggregateOption, | ||
NumericOptions extends string = NumericAggregateOption, | ||
R extends "aggregate" | "withPropertiesAggregate" = "aggregate", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ssanjay1 Is this what you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea that works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though maybe keeping parity with the other names it should be aggregate | derivedPropertiesAggregate
or something along those lines, basically including the word derive
@@ -30,19 +34,23 @@ export type NumericAggregateOption = | |||
| "approximateDistinct" | |||
| "exactDistinct"; | |||
|
|||
type AGG_FOR_TYPE<T> = number extends T ? NumericAggregateOption | |||
: string extends T ? StringAggregateOption | |||
type AGG_FOR_TYPE<T, U extends boolean> = number extends T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename U
so its clear what that boolean generic is for
: never; | ||
|
||
export type ValidAggregationKeys< | ||
Q extends ObjectOrInterfaceDefinition, | ||
StringOptions extends string = StringAggregateOption, | ||
NumericOptions extends string = NumericAggregateOption, | ||
R extends "aggregate" | "withPropertiesAggregate" = "aggregate", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though maybe keeping parity with the other names it should be aggregate | derivedPropertiesAggregate
or something along those lines, basically including the word derive
import type { LinkedType, LinkNames } from "../util/LinkUtils.js"; | ||
import type { WithPropertyDefinition } from "./WithPropertiesClause.js"; | ||
|
||
export interface WithPropertyObjectSet<Q extends ObjectOrInterfaceDefinition> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets be consistent with naming, see some withProperty
and derive
here.
| "avg" | ||
| BaseWithPropAggregations | ||
| CollectWithPropAggregations; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I am thinking about whats ideal we need to add some type tests. I'm okay if its in a follow up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few notes we should fix up. If we don't do the IR I think we should fix up the types as I mention to not carry so much complexity around.
I am still undecided on the IR or not. I get not wanting to create a complex language (or reproduce what the backend has) but I also feel like there are no surprises there either. IDK, this is a tough one.
> = { | ||
__DefinitionMetadata: { | ||
properties: { | ||
[T in Extract<keyof D, string>]: D[T] extends |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have to check but my gut is keyof D & string
is better for the compiler than the conditional type that Extract
causes.
) => ObjectSet< | ||
WithPropertiesObjectDefinition< | ||
Q, | ||
D |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with it for now but this feels like a place where having the simpler types that are not based on functions produces more readable types? We have to do the extraction work anyway to create things like properties
and props
below but we are required to keep the function hierarchy in the type generics as this object is passed around instead of resolving them at the time withProperties
is called and thus carrying a lot less. Like we could just be carrying { foo: number, bar: string }
lets say. Or even { foo: PropertyDefinition, bar: PropertyDefinition }
.
export type { | ||
WithPropertyObjectSet, | ||
} from "./derivedProperties/WithPropertyObjectSet.js"; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra new line causes grouping when organizing imports instead of perfect sorting.
@@ -485,6 +488,285 @@ describe("ObjectSet", () => { | |||
}); | |||
}); | |||
|
|||
describe("Derived Properties Object Set", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would want to see some expectTypeOf
entries for all of this. It both makes it easier to review and gives protections against the future that we didn't break things. Right now we just know that the incoming syntax is correct but not the output.
type: "methodInput", | ||
}, map); | ||
|
||
const clause: WithPropertiesClause<Employee> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For something like this, especially so you can add type tests you want this to be more like const clause = { ... } satisfies WithPropertiesClause<Employee>
so that it will capture the raw values
expect(objectWithUndefinedRdp.derivedPropertyName).toBeUndefined(); | ||
}); | ||
|
||
describe("withPropertiesObjectSet", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: don't pass this string. This test seems to be directly testing createWithPropertiesObjectSet
and so you can literally pass a reference to said function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this also means that this entire describe should really be in createWithPropertiesObjectSet.test.ts
so its clearer whats under test.
}; | ||
|
||
const result = clause["derivedPropertyName"](deriveObjectSet); | ||
const definition = map.get(result.definitionId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some mixed feelings about the definitionId
still, especially considering it ends up on the public api.
"Invalid aggregation operation " + aggregationOperation, | ||
); | ||
} | ||
definitionMap.set(definitionId.toString(), { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suppose for a moment definitionMap
is actually Map<object, DerivedPropertyDefinition>
.
instead of definitionId
we could do this:
const ret = {} as WithPropertyDefinition<any>;
definitionMap.set(ret, {... });
return ret;
No need to do accounting of ids and we don't have to leak them/make them part of our public API.
Its still not perfect because people might return their own objects thinking thats okay when its not and they still might do something weird like reuse an object from before (which won't work because its a different map) but its closer.
Ultimately I think we are probably better just designing an IR that is based on raw objects.
This PR adds the base for runtime derived properties in the OSDK.
The following are features to be added before the completion of the project but are not covered by this PR
selectProperty
,collectList
,collectSet
a === b ? integerProperty : stringProperty
. For this PR, enforcing that the user does a simple definition here is fine, but needs to be fixed in the future. Supportany
typed or narrowed properties will have the result of fixing this issue.